#!/usr/bin/perl -w

# Copyright (C) 2005, 2006, 2013 Apple Computer, Inc.  All rights reserved.
# Copyright (C) 2007 Holger Hans Peter Freyther.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1.  Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer. 
# 2.  Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution. 
# 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
#     its contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission. 
#
# THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Script to build, run and visualize coverage information

use strict;
use File::Basename;
use File::Spec;
use FindBin;
use Getopt::Long qw(:config pass_through);
use JSON;
use lib $FindBin::Bin;
use List::Util qw(sum);
use List::Util qw(max);
use POSIX;
use webkitdirs;
use XML::Simple;

sub parseGcovrOutput($);
sub getFileHitsAndBranches($);
sub addLineCounts($$$$$$);
sub createResultName();

my $resultName = createResultName();

# Move to the source directory
chdirWebKit();

# Delete old gcov files
print "Cleaning up\n";
system("if [ -d WebKitBuild ]; then find WebKitBuild -name '*.gcda' -delete; fi;") == 0 or die "Cannot delete old gcda files (code coverage";

# Compile WebKit and run the tests
print "Building and testing\n";
system("Tools/Scripts/build-webkit", "--coverage", @ARGV) == 0 or die "Cannot compile webkit with code coverage";
system("Tools/Scripts/run-webkit-tests");
system("Tools/Scripts/run-webkit-tests -2");
system("Tools/Scripts/run-javascriptcore-tests");
system("Tools/Scripts/run-api-tests");

# Generate the coverage data and report
print "Collecting coverage data\n";
system("mkdir WebKitBuild/Coverage") if ! -d "WebKitBuild/Coverage";
system("python Tools/Scripts/webkitpy/tool/gcovr --xml --output=WebKitBuild/Coverage/" . $resultName . ".xml") == 0 or die "Cannot run gcovr";

# Collect useful data from xml to json format
open my $jsonFile, ">", "WebKitBuild/Coverage/$resultName.json" or die "Cannot open $resultName.json";
print $jsonFile encode_json(parseGcovrOutput("WebKitBuild/Coverage/$resultName.xml"));
close $jsonFile;

print "Done\n";

sub parseGcovrOutput($)
{
    my ($xmlData) = @_;
    my $sourceDir = sourceDir();
    
    my @files;

    # The xml output of gcovr uses a Java-like package/class names for directories and files
    my $packages = new XML::Simple->XMLin($xmlData)->{"packages"}->{"package"};

    foreach my $packageName (keys %{$packages}) {
        my $classes = $packages->{$packageName}->{"classes"}->{"class"};
        
        # Perl's XML::Simple causes files to be here in the parsed xml data structure
        # if there's only one child, even though they're a layer deeper in the xml tree
        if ($classes->{"filename"} && $classes->{"lines"}) {
            if ($classes->{"filename"} =~ /$sourceDir/) {
                push(@files, getFileHitsAndBranches($classes));
            }
        }
        else {
            foreach my $key (keys %{$classes}) {
                my $class = $classes->{$key};
                if ($class->{"filename"} =~ /$sourceDir/) {
                    push(@files,getFileHitsAndBranches($class));
                }
            }
        }
    }
    return \@files;
}

sub getFileHitsAndBranches($)
{
    my ($class) = @_;

    my @hits;
    my @hitLines;
    my @branchesPossible;
    my @branchesTaken;
    my @branchLines;

    my $lines = $class->{"lines"}->{"line"};
    if (ref($lines) eq "ARRAY") {
        foreach my $line (@$lines) {
            addLineCounts($line, \@hits, \@hitLines, \@branchesPossible, \@branchesTaken, \@branchLines);
        }
    } else {
        addLineCounts($lines, \@hits, \@hitLines, \@branchesPossible, \@branchesTaken, \@branchLines);
    }
    
    my $file = {};
    $file->{"hits"} = \@hits;
    $file->{"hitLines"} = \@hitLines;
    $file->{"branchesPossible"} = \@branchesPossible;
    $file->{"branchesTaken"} = \@branchesTaken;
    $file->{"branchLines"} = \@branchLines;
    $file->{"filename"} = substr($class->{"filename"}, length(sourceDir()));
    $file->{"coverage"} = abs($class->{"line-rate"});
    if (@branchLines) {
        $file->{"branchCoverage"} = abs($class->{"branch-rate"});
    } else {
        $file->{"branchCoverage"} = 1;
    }
    $file->{"totalHeat"} = sum(@hits);
    $file->{"maxHeat"} = max(@hits);
    return $file;
}

sub addLineCounts($$$$$$)
{
    my ($line, $hits, $hitLines, $branchesPossible, $branchesTaken, $branchLines) = @_;
    push(@$hits, int($line->{"hits"}));
    push(@$hitLines, int($line->{"number"}));
    if($line->{"branch"} eq "true") {
    
        # Extract the numerator and denominator of the condition-coverage attribute, which looks like "75% (3/4)"
        $line->{"condition-coverage"} =~ /\((.*)\/(.*)\)/;
        push(@$branchesTaken, int($1));
        push(@$branchesPossible, int($2));
        push(@$branchLines, int($line->{"number"}));
    }
}

sub createResultName()
{
    my $svnVersion = determineCurrentSVNRevision();
    my @timeData = localtime(time);
    return $svnVersion . "-" . join('_', @timeData);
}
